package com.boss.core.util;

import com.boss.core.BossConstants;
import com.boss.core.manage.BossEnvironment;
import com.boss.core.manage.BossPort;
import com.boss.core.manage.BossServer;
import com.michelin.cio.hudson.plugins.rolestrategy.Role;
import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy;
import hudson.model.Computer;
import hudson.model.Executor;
import hudson.model.Hudson;
import hudson.model.User;
import hudson.model.Queue.Item;

import java.io.File;
import java.io.FileInputStream;
import java.util.*;
import java.util.regex.Pattern;

import hudson.security.AuthorizationStrategy;
import hudson.security.GlobalMatrixAuthorizationStrategy;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;

/**
 * Provides utility methods for BOSS.
 *
 * @author Momin Noor Khan
 */
public final class BossUtil extends BossBaseUtil {
    /**
     * To be used when we have jobs on Hudson catering to a project directly and not limited to only a release.
     * Note that in this case the strategy is that we are having multiple clones of the project specific job.
     * This means that one of the available clones would be fetched by this method.
     * The job is generic and we have clones of that job on Hudson available to be used in case an instance
     * is already in use.Thus, we would not need to use 'Execute Concurrent Builds' feature on job configuration page.
     * For example lets say that for CPM we have a job CPM-1 and CPM-2 as its clone.
     * Now when a CPM build/deploy order is submitted we look for an available job
     * from the list of candidate jobs to carry	out this task.
     * Here, it would first look up whether CPM-1 is available or not.
     * If it is available then the job would be triggered.
     * Otherwise, it would look for the availability of the next available candidate
     * By availability it is meant that the job is not in the build queue or build executor list.
     * If a job is in build executor list and no other jobs are available at that time
     * then the current order submission would use that job and add it to the build queue.
     * It is to be noted that currently we are using a mechanism where we are deliberately delaying
     * a submitted job in the build queue first by using 'delay' parameter even when all the executors might be free.
     * This mechanism is different from the default functionality of Hudson.
     */

    public static String findAvailableJob(String project) {
        if (!StringUtils.isBlank(project)) {
            // Get names of all jobs
            Collection<String> allJobs = Hudson.getInstance().getJobNames();

            // Get names of jobs in build queue (i.e. pending, quiet, waiting)
            Item[] queueItems = Hudson.getInstance().getQueue().getItems();

            // Find matching jobs from all job names' list using 'project' name as the regex
            List<String> candidateJobs = new ArrayList<String>();
            candidateJobs = getCandidateJobs(allJobs, project);

            // Check whether a candidate is free or not
            boolean isCandidateFree = true;
            List<String> tempCandidates = new ArrayList<String>();
            String tempCandidate = "";

            outer:
            for (String candidate : candidateJobs) {
                isCandidateFree = false;

                inner1:
                for (Item item : queueItems) {
                    if (candidate.equals(item.task.getFullDisplayName())) {
                        isCandidateFree = false;
                        continue outer;
                    }
                }

                tempCandidate = candidate;
                tempCandidates.add(tempCandidate);
                //at this point candidate is not there in build queue, now we need to check this in build executors' list too

                Computer[] comps = Hudson.getInstance().getComputers();
                inner2:
                for (Computer c : comps) {
                    List<Executor> executors = c.getExecutors();
                    for (Executor e : executors) {
                        if (e.isBusy()) {
                            if (candidate.equals(e.getCurrentExecutable().getParent().getDisplayName())) {
                                isCandidateFree = false;
                                continue outer;
                            }
                        }
                    }
                }

                tempCandidate = candidate;  // ?why using this here
                tempCandidates.add(tempCandidate); //? same as above

                //we dont need this assignment actually but it has been put here to keep myself reminded that at this point in any case the candidate must be free.
                //remove it later on and do the final testing once the above logic gets correct
                isCandidateFree = true;
                //if we reach here it means the candidate is free
                if (isCandidateFree) {
                    return candidate;
                }
            }
            //if we return from here it means none of the candidates are free , so use one of them forcefully
            // as no other option is left for us.

            return getJobNameForcefully(tempCandidates);
        }

        //if we are returning from here it means project is null/empty and hence no job is to be assigned in this case
        return BossConstants.JOB_INVALID;
    }

    public static List<String> getCandidateJobs(Collection<String> allJobs, String project) {
        List<String> candidateJobs = new ArrayList<String>();
        for (String jobName : allJobs) {
            boolean matchingJobFound = Pattern.matches(project + "\\-\\d",
                    jobName);

            if (matchingJobFound) {
                candidateJobs.add(jobName);
            }
        }
        return candidateJobs;
    }

    public static String getJobNameForcefully(List<String> temporaryCandidates) {
        String result = "";

        // from temp list return the first entry.
        //remove is used because it returns the value, removes the entry and shifts the remaining list accordingly
        if (!temporaryCandidates.isEmpty()) {
            result = temporaryCandidates.remove(0);
        } else {
            //if we are returning from here it means no candidate job is available at the moment
            result = BossConstants.JOB_NOT_AVAILABLE;
        }
        return result;
    }

    /**
     * Gets the available job for the given project
     *
     * @param project
     * @return
     */
    public static String findAvailableJob2(String project) {
        if (!StringUtils.isBlank(project)) {
            // Get names of all jobs
            Collection<String> allJobs = Hudson.getInstance().getJobNames();

            // Get names of jobs in build queue (i.e. pending, quiet, waiting)
            Item[] queueItems = Hudson.getInstance().getQueue().getItems();

            // Find matching jobs from all job names' list using 'project' name as the regex
            for (String jobName : allJobs) {
                boolean matchingJobFound = Pattern.matches(project + "\\-\\d",
                        jobName);

                if (matchingJobFound) {
                    return jobName;
                }
            }
        }
        return BossConstants.JOB_INVALID;
    }

    /**
     * Gets the value(s) of the key from the properties file in the form of a list with first item in the list as 'dummy' value
     * using the specified delimiter.
     *
     * @param key
     * @param pathToPropertyFile
     * @return
     */
    public static List<String> getPropertyValues(String key, String pathToPropertyFile, char delimiter, boolean isDummyValueRequired, boolean isDefaultFirstRequired) {
        String propertyFile = pathToPropertyFile;
        String propertyKey = key;

        FileInputStream fIS = null;
        String propertyValue = "";
        List<String> resultList = new ArrayList<String>();


        if (!StringUtils.isBlank(propertyFile) && !StringUtils.isBlank(propertyKey)) {
            try {
                fIS = new FileInputStream(new File(propertyFile));
                Properties properties = new Properties();
                properties.load(fIS);
                propertyValue = StringUtils.strip(properties.getProperty(propertyKey));
                String[] values = StringUtils.split(propertyValue, delimiter);

                for (String value : values) {
                    //safety check - remove trailing spaces (if any) from the value and add it to the list
                    resultList.add(StringUtils.strip(value));
                }

                //sort the list in ascending order
                Collections.sort(resultList);

                //add default value as first item of the list if required
                if (isDefaultFirstRequired) {
                    String defaultValue = getPropertyValue(key + "-default", propertyFile);
                    if (resultList.contains(defaultValue)) {
                        int index = resultList.indexOf(defaultValue);
                        String temp = resultList.remove(index);
                        resultList.add(0, temp);
                    }
                }

                //add dummy value as first item of the SORTED list if required
                if (isDummyValueRequired) {
                    resultList.add(0, BossConstants.DUMMY_VALUE);
                }

                //return resultList;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                IOUtils.closeQuietly(fIS);
            }
        }

        return resultList;
    }

    public static List<String> getPropertyValues2(String propertySectionName, String propertyKey, char delimiter, boolean isDummyValueRequired, boolean isDefaultFirstRequired) {
        String propertyValue = "";
        List<String> resultList = new ArrayList<String>();

        if (!BossUtil.isBlankOrDummy(propertySectionName) && !BossUtil.isBlankOrDummy(propertyKey)) {
            try {
                propertyValue = StringUtils.strip(BossGlobalUtil.getPropertyValue(propertySectionName, propertyKey));
                String[] values = StringUtils.split(propertyValue, delimiter);

                if (values != null) {
                    for (String value : values) {
                        //safety check - remove trailing spaces (if any) from the value and add it to the list
                        resultList.add(StringUtils.strip(value));
                    }
                }

                //sort the list in ascending order
                Collections.sort(resultList);

                //add default value as first item of the list if required
                if (isDefaultFirstRequired) {
                    String defaultValue = BossGlobalUtil.getPropertyValue(propertySectionName, "DEFAULT-" + propertyKey);
                    if (defaultValue != null) {
                        defaultValue = StringUtils.strip(defaultValue);
                        if (resultList.contains(defaultValue)) {
                            int index = resultList.indexOf(defaultValue);
                            String temp = resultList.remove(index);
                            resultList.add(0, temp);
                        }
                    }
                }

                //add dummy value as first item of the SORTED list if required
                if (isDummyValueRequired) {
                    resultList.add(0, BossConstants.DUMMY_VALUE);
                }

                //return resultList;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return resultList;
    }


    /**
     * Gets the value of the property key from the specified properties file
     *
     * @param key
     * @param pathToPropertyFile
     * @return
     */
    public static String getPropertyValue(String key, String pathToPropertyFile) {
        String propertyFile = pathToPropertyFile;
        String propertyKey = key;

        FileInputStream fIS = null;
        String propertyValue = "";

        if (!StringUtils.isBlank(propertyFile) && !StringUtils.isBlank(propertyKey)) {
            try {
                fIS = new FileInputStream(new File(propertyFile));
                Properties properties = new Properties();
                properties.load(fIS);
                propertyValue = StringUtils.strip(properties.getProperty(propertyKey));
                //return propertyValue;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                IOUtils.closeQuietly(fIS);
            }
        }
        return propertyValue;
    }

    public static String getUrlParam(boolean hasBlock, String key, String value) {
        String urlParam = "";
        if (hasBlock) {
            if (!StringUtils.isBlank(value)) {
                urlParam = "&" + key + "=" + value;
            }
        }
        return urlParam;
    }

    public static String getUrlParam(boolean hasBlock, String key) {
        String urlParam = "&" + key + "=" + String.valueOf(hasBlock);
        return urlParam;
    }

    public static String getUrlParam(String key, String value) {
        String urlParam = "";
        if (!StringUtils.isBlank(value)) {
            urlParam = "&" + key + "=" + value;
        }
        return urlParam;
    }

    public static String encodeQueryString(String urlParams) {
        String queryString = urlParams.replace(" ", "%20");
        return queryString;
    }

    public static String encodeQueryString2(String argString) {
        StringBuilder uri = new StringBuilder();
        String mark = "-_.!~*'()\"";

        char[] chars = argString.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char chr = chars[i];
            if (isDigit(chr) || isSmallLetter(chr) || isCapitalLetter(chr) || mark.indexOf(chr) != -1) {
                uri.append(chr);
            } else {
                uri.append("%");
                uri.append(Integer.toHexString((int) chr));
            }
        }
        return uri.toString();
    }

    public static boolean isDigit(char chr) {
        if (chr >= '0' && chr <= '9') {
            return true;
        }
        return false;
    }

    public static boolean isSmallLetter(char chr) {
        if (chr >= 'a' && chr <= 'z') {
            return true;
        }
        return false;
    }

    public static boolean isCapitalLetter(char chr) {
        if (chr >= 'A' && chr <= 'Z') {
            return true;
        }
        return false;
    }

    public static List<BossEnvironment> getUniqueEnvironments(List<BossEnvironment> tmpEnvList) {
        List<BossEnvironment> resultEnvList = new ArrayList<BossEnvironment>();
        for (BossEnvironment tmpEnv : tmpEnvList) {
            if (StringUtils.isBlank(tmpEnv.getEnvironmentName())
                    ||
                    tmpEnv.getEnvironmentName().equals(BossConstants.DUMMY_VALUE)) {
                continue;
            }

            if (!resultEnvList.contains(tmpEnv)) {
                tmpEnv.setServers(getUniqueServers(null, tmpEnv));
                resultEnvList.add(tmpEnv);
                continue;
            }

            for (BossEnvironment env : resultEnvList) {
                if (env.getEnvironmentName().equals(tmpEnv.getEnvironmentName())) {
                    env.setServers(getUniqueServers(env, tmpEnv));
                }
            }
        }

        if (!resultEnvList.isEmpty()) {
            Collections.sort(resultEnvList);
        }
        return resultEnvList;
    }

    public static List<BossServer> getUniqueServers(BossEnvironment environment, BossEnvironment tmpEnvironment) {
        List<BossServer> tmpSvrList = tmpEnvironment.getServers();
        List<BossServer> resultSvrList = new ArrayList<BossServer>();

        if (environment != null) {
            resultSvrList = environment.getServers();
        }

        if (tmpSvrList != null) {
            for (BossServer tmpSvr : tmpSvrList) {
                if (StringUtils.isBlank(tmpSvr.getServerName())
                        ||
                        tmpSvr.getServerName().equals(BossConstants.DUMMY_VALUE)) {
                    continue;
                }

                if (!resultSvrList.contains(tmpSvr)) {
                    tmpSvr.setPorts(getUniquePorts(null, tmpSvr));
                    resultSvrList.add(tmpSvr);
                    continue;
                }

                for (BossServer svr : resultSvrList) {
                    if (svr.getServerName().equals(tmpSvr.getServerName())) {
                        svr.setPorts(getUniquePorts(svr, tmpSvr));
                    }
                }
            }
        }

        if (!resultSvrList.isEmpty()) {
            Collections.sort(resultSvrList);
        }
        return resultSvrList;
    }

    public static List<BossPort> getUniquePorts(BossServer svr, BossServer tmpSvr) {
        List<BossPort> tmpPortList = tmpSvr.getPorts();
        List<BossPort> resultPortList = new ArrayList<BossPort>();

        if (svr != null) {
            resultPortList = svr.getPorts();
        }

        if (tmpPortList != null) {
            for (BossPort tmpPort : tmpPortList) {
                if (StringUtils.isBlank(tmpPort.getPortName())
                        ||
                        tmpPort.getPortName().equals(BossConstants.DUMMY_VALUE)) {
                    continue;
                }

                if (!resultPortList.contains(tmpPort)) {
                    resultPortList.add(tmpPort);
                }
            }
        }

        if (!resultPortList.isEmpty()) {
            Collections.sort(resultPortList);
        }
        return resultPortList;
    }

    public static boolean isUserAuthorizedForAllContainers(String userName) {
        if (isGlobalAdmin(userName)
                ||
                isUserInBossRole(userName, BossConstants.ROLE_ADMIN)
                ||
                isUserInBossRole(userName, BossConstants.ROLE_SUPER_USER)) {
            return true;
        }
        return false;
    }

    public static boolean isGlobalAdmin(String userName) {
        AuthorizationStrategy authStrategy = Hudson.getInstance().getAuthorizationStrategy();

        //We are using matrix-based authorization strategy only
        //a generic approach would be to handle other strategies gracefully too instead of returning false for them
        //but for the time being the following would suffice
        if (authStrategy instanceof GlobalMatrixAuthorizationStrategy) {
            if (((GlobalMatrixAuthorizationStrategy) authStrategy).hasPermission(userName, Hudson.ADMINISTER)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if user is in a boss role
     * (based on role-strategy authorization)
     *
     * @param userName
     * @param roleName
     * @return
     */
    public static boolean isUserInBossRole(String userName, String roleName) {
        AuthorizationStrategy authStrategy = Hudson.getInstance().getAuthorizationStrategy();
        User user = Hudson.getInstance().getUser(userName);
        if (user == null) {
            return false;
        }
        if (authStrategy instanceof RoleBasedAuthorizationStrategy) {
            RoleBasedAuthorizationStrategy roleBasedAuthStrategy = (RoleBasedAuthorizationStrategy) authStrategy;
            //??Collection<String> groups = roleBasedAuthStrategy.getGroups();
            //??ACL acl = roleBasedAuthStrategy.getACL(user);
            SortedMap<Role, Set<String>> grantedRoles = roleBasedAuthStrategy.getGrantedRoles("globalRoles");
            for (Map.Entry<Role, Set<String>> entry : grantedRoles.entrySet()) {
                if (entry.getKey().getName().equals(roleName)) {
                    Set<String> set = entry.getValue();
                    if (set.contains(userName)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
